package com.unitd.frame.sso.service.sso.impl;

import com.alibaba.fastjson.JSON;
import com.unitd.frame.sso.common.config.SSOConfig;
import com.unitd.frame.sso.common.helper.CookieHelper;
import com.unitd.frame.sso.common.token.AuthToken;
import com.unitd.frame.sso.common.token.Token;
import com.unitd.frame.comm.utils.http.HttpUtils;
import com.unitd.frame.comm.utils.RandomUtil;
import com.unitd.frame.sso.service.cache.ISSOCache;
import com.unitd.frame.sso.service.sso.ISSOService;
import com.unitd.frame.sso.service.statistic.ISSOStatistic;
import com.unitd.frame.comm.utils.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @desc SSO 单点登录服务抽象实现类
 * @filename SSOServiceImpl.java
 * @copyright www.unitd.com
 * @author Hudan
 * @version 1.0
 * @date 2016/10/14
 */
public class SSOServiceImpl extends SSOServiceSupport implements ISSOService {

	public SSOServiceImpl() {
		if(config == null) {
			config = SSOConfig.getInstance();
		}
	}

	/**
	 * @desc 在当前访问域下,设置当前登录用户的 登录 Cookie
	 * request.setAttribute(SSOConfig.SSO_COOKIE_MAXAGE, -1);
	 * 可以设置 Cookie 超时时间 ,默认读取配置文件数据
	 * -1 浏览器关闭时自动删除 0 立即删除 120 表示Cookie有效期2分钟(以秒为单位)
	 * @param request  http请求对象
	 * @param response http响应对象
	 * @param token    SSO登录票据对象
	 */
	@Override
	public void setSSOCookie(HttpServletRequest request, HttpServletResponse response, Token token) {
		setSSOCookie(request, response, token, config.getEncrypt());
	}

	/**
	 * @desc 当前访问域下设置登录Cookie 设置防止伪造SESSIONID攻击
	 * @param request  http请求对象
	 * @param response http相应对象
	 * @param token
	 */
	@Override
	public void authSSOCookie(HttpServletRequest request, HttpServletResponse response, Token token) {
		CookieHelper.authJSESSIONID(request, RandomUtil.getCharacterAndNumber(8));
		setSSOCookie(request, response, token);
	}

	/**
	 * @desc 获取当前通过SSO登录的在线人数(总数)
	 * @param request 查询请求
	 * @return 当前通过SSO登录的在线人数
	 */
	@Override
	public String getLoginCount(HttpServletRequest request) {
		ISSOStatistic statistic = config.getStatistic();
		if (statistic != null) {
			return statistic.count(request);
		} else {
			logger.error("请实例化ISSOStatistic接口对象或者自定义一个统计类并实现ISSOStatistic接口!");
		}
		return null;
	}

	/**
	 * @desc 退出SSO 登录
	 * @param request  http请求对象
	 * @param response http相应对象
	 * @throws IOException
	 */
	@Override
	public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException {
		// 退出登录,并删除cookie
		logout(request, response, config.getCache());

		// 重定向到退出登录页
		String logoutUrl = config.getLogoutUrl();
		if(StringUtils.isBlank(logoutUrl)) {
			response.getWriter().write("sso.properties配置文件中必须包含so.logout.url属性!");
		} else {
			response.sendRedirect(logoutUrl);
		}
	}

	/**
	 * @desc 根据指定的用户 ID 踢出该登录用户,退出当前系统
	 * @param userId 用户ID
	 * @return boolean 踢出登录成功与否
	 */
	@Override
	public boolean kickLogin(Object userId) {
		ISSOCache cache = config.getCache();
		if (cache != null) {
			return cache.delete(SSOConfig.toCacheKey(userId));
		} else {
			logger.info("请先自定义实现ISSOCache SSO 缓存接口的实现类,然后才能实现从缓存中踢出指定用户的功能!");
		}
		return false;
	}

	/**
	 * @desc 退出登录并清理当前登录状态;清理 Cookie、缓存、统计、等数据,但不进行重定向
	 * @param request  http请求对象
	 * @param response http相应对象
	 * @return boolean 退出登录成功与否
	 */
	@Override
	public boolean clearLogin(HttpServletRequest request, HttpServletResponse response) {
		return logout(request, response, config.getCache());
	}

	/**
	 * @desc 退出登录并清理当前登录状态;清理 Cookie、缓存、统计、等数据;
	 * 完成后,重定向到sso.properties 配置文件中的属性 sso.login.url 指定的登录URL地址
	 * 一般情况下,使用此方法作为登出处理
	 * @param request  http请求对象
	 * @param response http相应对象
	 * @throws IOException
	 */
	@Override
	public void clearRedirectLogin(HttpServletRequest request, HttpServletResponse response) throws IOException {

		// 清理当前登录状态
		clearLogin(request, response);

		// 重定向到sso.properties配置文件中sso.login.url属性设置的登录页面
		String loginUrl = config.getLoginUrl();
		if(StringUtils.isBlank(loginUrl)) {
			response.getWriter().write("sso.properties配置文件中必须包含sso.login.url属性!");
		} else {
			String retUrl = HttpUtils.getQueryString(request, config.getEncoding());
			logger.info("重定向到登录页:" + retUrl);
			response.sendRedirect(HttpUtils.encodeRetURL(loginUrl, config.getParamReturl(), retUrl));
		}
	}

	/* ************************** 客户端处理Token相关方法 ************************** */
	/**
	 * @desc 获取当前请求 Token(从 Cookie中解密 token )
	 * 使用场景:拦截器下使用; 非拦截器建议使用 attrToken 减少二次解密
	 * @param request http请求对象
	 * @return Token {@link Token}
	 */
	@Override
	public Token getToken(HttpServletRequest request) {
		return getToken(request, config.getEncrypt(), config.getCache());
	}


	/* ************************** 跨域处理相关方法 **************************
	 * 1、业务系统访问 SSO 保护系统、 验证未登录跳转至 SSO系统
	 * 2、SSO 设置信任Cookie 生成询问密文,通过代理页面 JSONP 询问业务系统是否允许登录
	 * 3、业务系统验证询问密文生成回复密文.
	 * 4、代理页面 getJSON SSO 验证回复密文,SSO 根据 ok 返回userId 查询绑定关系进行登录,通知代理页面重定向到访问页面.
	*/
	/**
	 * @desc 根据RSA私钥生成对应的跨域询问密文(公钥, 用于跨域处理)
	 * @param request    http请求对象
	 * @param response   http相应对象
	 * @param privateKey 私钥
	 * @return 跨域密文
	 */
	@Override
	public AuthToken askCiphertext(HttpServletRequest request, HttpServletResponse response, String privateKey) {

		// 利用私钥进行RSA签名,获取授权签名Token
		AuthToken at = new AuthToken(request, privateKey);

		// 设置跨域临时信任 Cookie
		this.setAuthCookie(request, response, at);
		return at;
	}

	/**
	 * @desc 根据请求的询问密文生成对应的跨域回复密文(公钥, 用于跨域处理)
	 * @param request
	 * @param askData 询问密文  @return AuthToken {@link AuthToken}
	 */
	@Override
	public AuthToken replyCiphertext(HttpServletRequest request, String askData) {
		String str = null;
		try {
			str = config.getEncrypt().decrypt(askData, config.getAuthCookieSecretkey());
		} catch (Exception e) {
			logger.error("replyCiphertext方法进行AES解密错误:" + e.toString());
		}

		if (str != null) {
			// 使用业务系统公钥验证签名,验证 IP 地址是否合法
			AuthToken at = JSON.parseObject(str, AuthToken.class);
			if (checkIp(request, at) != null) {
				return at;
			}
		}
		return null;
	}

	/**
	 * @desc 验证跨域回复密文的有效性, 如果成功则返回绑定的用户ID 等信息
	 * @param request   http请求对象
	 * @param response  http相应对象
	 * @param replyData 回复密文
	 * @param ssoClientPrk  RSA 公钥 (业务系统公钥，验证authToken签名, SSO客户端自有的公钥)
	 * @param ssoServerPro  RSA 公钥 (SSO 回复密文公钥验证签名, SSO服务端对应发布的公钥)
	 * @return AuthToken {@link AuthToken}
	 */
	public AuthToken ok(HttpServletRequest request, HttpServletResponse response, String replyData, String ssoClientPrk,
						String ssoServerPro) {
		AuthToken token = getAuthCookie(request, ssoClientPrk);
		if (token != null) {
			String rt = null;
			try {
				rt = config.getEncrypt().decrypt(replyData, config.getAuthCookieSecretkey());
			} catch (Exception e) {
				logger.error("采用AES加密SSO出错:" + e.getMessage());
				e.printStackTrace();
			}
			if (rt != null) {
				AuthToken at = JSON.parseObject(rt, AuthToken.class);
				if (at != null && at.getUuid().equals(token.getUuid())) {
					if (at.verify(ssoServerPro) != null) {
						// 删除跨域信任Cookie 返回 userId
						CookieHelper.clearCookieByName(request, response, config.getAuthCookieName(),
								config.getCookieDomain(), config.getCookiePath());
						return at;
					}
				}
			}
		}
		return null;
	}

	/**
	 * @desc 设置跨域信任 Cookie
	 * @param authToken 跨域信任 Token
	 */
	private void setAuthCookie(HttpServletRequest request, HttpServletResponse response, AuthToken authToken) {
		try {
			CookieHelper.addCookie(response, config.getCookieDomain(), config.getCookiePath(),
					config.getAuthCookieName(), encryptCookie(request, authToken, config.getEncrypt()),
					config.getAuthCookieMaxage(), true, config.getCookieSecure());
		} catch (Exception e) {
			logger.error("加密跨域票据的cookie失败:" + e.toString());
		}
	}

	/**
	 * @desc 获取跨域信任临时 Cookie 保存的 AuthToken 票据
	 * 验证存在并且 IP 地址正确,签名合法
	 * @param request    http请求对象
	 * @param publicKey RSA 公钥(业务系统公钥验证签名合法)
	 * @return AuthToken {@link AuthToken}
	 */
	private AuthToken getAuthCookie(HttpServletRequest request, String publicKey) {
		String jsonToken = getJsonToken(request, config.getEncrypt(), config.getAuthCookieName());

		if(StringUtils.isBlank(jsonToken)) {
			logger.info("跨域信任Token为空!");
			return null;
		} else {
			// 校验 IP 合法返回 AuthToken
			AuthToken at = JSON.parseObject(jsonToken, AuthToken.class);
			if (checkIp(request, at) == null) {
				return null;
			}
			return at;
		}
	}
}